package org.xx.armory.rpc.decorators;

import org.xx.armory.bindings.BoundParamCollection;
import org.xx.armory.commons.SignHelper;
import org.xx.armory.commons.StringEnum;
import org.xx.armory.commons.ValueEnum;
import org.xx.armory.rpc.Decorator;
import org.xx.armory.security.KeyStorage;
import org.xx.armory.service.Invocation;
import org.xx.armory.service.ServiceContext;
import org.xx.armory.service.ServiceException;
import org.xx.armory.service.WithSignature;

import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.util.Date;

import static java.util.Arrays.stream;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.xx.armory.commons.SysUtils.stringEquals;

/**
 * 用于对参数进行完整性验证的修饰器。
 */
public class VerifyParamsDecorator
        extends AbstractDecorator
        implements Decorator {

    /**
     * 将参数值转为字符串形式。
     * <p>HTTP协议发送表单时，每个域都必须以文本形式发送，所以需要先将参数值转为字符串。</p>
     * <table>
     * <tr>
     * <th>类型</th>
     * <th>转化方式</th>
     * </tr>
     * <tr>
     * <td>null</td>
     * <td>空字符串({@literal ""})</td>
     * </tr>
     * <tr>
     * <td>{@link CharSequence}</td>
     * <td>调用{@link Object#toString()}方法。</td>
     * </tr>
     * </table>
     *
     * @param value
     *         参数值。
     * @return 字符串形式。
     * @throws IllegalArgumentException
     *         如果参数{@code value}的类型无法识别。
     */
    protected String paramValueToString(
            Object value
    ) {
        if (value == null) {
            return "";
        } else if (value instanceof CharSequence) {
            return value.toString();
        } else if (value instanceof BigDecimal) {
            return ((BigDecimal) value).toPlainString();
        } else if (value instanceof Number) {
            return value.toString();
        } else if (value instanceof Boolean) {
            return ((Boolean) value) ? "true" : "false";
        } else if (value instanceof Date) {
            return String.valueOf(((Date) value).getTime());
        } else if (value instanceof ValueEnum) {
            return String.valueOf(((ValueEnum) value).longValue());
        } else if (value instanceof StringEnum) {
            return ((StringEnum) value).stringValue();
        } else if (value.getClass().isArray()) {
            final StringBuilder buffer = new StringBuilder();
            for (int i = 0; i < Array.getLength(value); ++i) {
                buffer.append(paramValueToString(Array.get(value, i)));
            }
            return buffer.toString();
        } else {
            throw new IllegalArgumentException("cannot convert parameter " + value + " to string");
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void before(
            Invocation invocation,
            BoundParamCollection params,
            ServiceContext serviceContext
    )
            throws Exception {
        final WithSignature withSignature = invocation.getMethod().getAnnotation(WithSignature.class);
        if (withSignature != null && !isEmpty(withSignature.signatureName())) {
            // 如果被调用的方法加入了{@link WithSignature}注解，并且该注解生效
            // 那么执行完整性校验。
            final String signature = String.valueOf(params.get(withSignature.signatureName()));

            String[] paramNames = withSignature.paramNames();
            if (paramNames.length == 0) {
                paramNames = params.names().toArray(new String[0]);
            }

            SignHelper signHelper = SignHelper.getInstance();

            final String paramsString = stream(paramNames)
                    .filter(paramName -> !stringEquals(paramName, withSignature.signatureName()))
                    .map(params::get)
                    .filter(paramValue -> !(paramValue instanceof ServiceContext))
                    .map(this::paramValueToString)
                    .reduce(new StringBuilder(), StringBuilder::append, StringBuilder::append)
                    .toString();

            if (!signHelper.verifyBase64(paramsString, signature, KeyStorage.getInstance().getRSAPublicKey("verify-params-public"))) {
                throw new ServiceException("verify parameters failed");
            }
        }
    }
}
